package com.jketing.controller;

import java.beans.PropertyEditorSupport;
import java.io.IOException;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jketing.Config;
import com.jketing.Constants;
import com.jketing.bean.PageBean;
import com.jketing.entity.BaseEntity;
import com.jketing.service.BaseService;
import com.jketing.utils.BeanUtil;
import com.jketing.utils.DateUtils;

/**
 * BaseController 请求处理基类
 * 
 * 实现Controller层的一些公共方法
 * 
 * @author  李荣华
 * @version v1.0
 * @date    2017-11-22
 * @since   Spring-mvc 4.3.8
 */
public abstract class BaseController<T extends BaseEntity,ID extends Serializable> {
	
	/**
	 *  baseService 服务层引用  
	 */
	private BaseService<T,ID> baseService;
	/**
	 *  modelClass 实体模型类  
	 */
	private Class<T> modelClass;
	private boolean executeSave;
	/**
	 *  shortName 实体首字小写短类名  
	 */	  
	protected String shortName;
	/**
	 * 日志对象
	 */
	protected Logger logger = LoggerFactory.getLogger(getClass());
	/**
	 *  HttpServletRequest request 由IOC自动注入HttpServletRequest  
	 */
	@Autowired  
	protected HttpServletRequest request;	
	
	/**
	 *  BaseController 无参构造器  
	 *  
	 */
	public BaseController() {
		//得到当前对象的参数化类型的参数
		modelClass = BeanUtil.getSuperGenericClass(getClass());
		//实体类首字小写短名
		shortName = ClassUtils.getShortNameAsProperty(modelClass);
		executeSave = true;
	}
	
	/**
	 *  setBaseService 以set方式注入BaseService  
	 */
	public void setBaseService(BaseService<T, ID> service) {
		baseService = service;
	}

	/**
	 *  setExecuteSave 设置是否执行基类的保存操作，默认是执行  
	 */
	public void setExecuteSave(boolean executeSave) {
		this.executeSave = executeSave;
	}

	/**
	 *  getSession 得到HttpSession
	 */
	protected HttpSession getSession() {
		return request.getSession();
	}
	
	/**
	 *  getPrefixPath 子类必顺实现，用于返回页面路径前缀字符串
	 *    
	 *  @return String
	 */
	protected abstract String getPrefixPath();
	/**
	 *  getQueryMode 子类必顺实现，用于返回查询方式
	 *  
	 *  取值有两种：Entity按实体查;String按字符串表达式查
	 *    
	 *  @return String
	 */
	protected abstract String getQueryMode();
	/**
	 *  getQuery 得到查询实体
	 *  
	 *  可在查询之前,再次设置查询条件
	 *    
	 *  @return T
	 */
	protected T getQuery(T query) {
		return query;
	}
	/**
	 *  getEntity 得到查询实体
	 *  
	 * 用于修改@ModelAttribute在设置之前的模型数据
	 * 若实体的关联对象要变更，例如有员工实体引用了部门实体，在员工编辑页修改了部门提交;
	 * 在此要将员工关联部门对象设为null,不为null调用员工更新方法会报如下异常：
	 * HibernateException: identifier of an instance of Dictionary was altered 
	 * from PK20171228174623535117 to PK20171228174623541118
	 * 调用形式：employee.setOrgan(null);
	 * 在此方法中增加设null关联对象代码
	 *  
	 *  可在查询之前,再次设置查询条件
	 *    
	 *  @return T
	 */
	protected T getEntity(T entity) {
		return entity;
	}
	/**
	 *  getExpression 得到查询表达式
	 *  
	 *  可在查询之前,再次调整查询表达式
	 *    
	 *  @return String
	 */
	protected String getExpression(String expression) throws JsonProcessingException{
		return expression;
	};
	/**
	 *  initSingleSelect 进入单选对话框前,做页面初始化处理
	 *  
	 *  @param  Map<String, Object> map jsp请求范围响应参数
	 *  
	 *  @return void
	 */
	protected void initSingleSelect(Map<String, Object> map) {}
	/**
	 *  initMultiSelect 进入多选对话框前,做页面初始化处理
	 *  
	 *  @param  Map<String, Object> map jsp请求范围响应参数
	 *  
	 *  @return void
	 */
	protected void initMultiSelect(Map<String, Object> map) {}
	/**
	 *  initListFrame 初始化列表框架页
	 *  
	 *  进入页面之前需要加载的数据
	 *  
	 *  @param  Map<String, Object> map jsp请求范围响应参数
	 *  
	 *  @return void
	 */
	protected void initListFrame(Map<String, Object> map) {}
	/**
	 *  initList 初始化列表页
	 *  
	 *  进入页面之前需要加载的数据,如查询选择列表数据加载
	 *  
	 *  @param  Map<String, Object> map jsp请求范围响应参数
	 *  
	 *  @return void
	 */
	protected void initList(Map<String, Object> map) {}
	/**
	 *  initView 初始化视图页
	 *  
	 *  进入页面之前需要加载的数据,如查询选择列表数据加载
	 *  
	 *  @param  Map<String, Object> map jsp请求范围响应参数
	 *  
	 *  @return void
	 */
	protected void initView(Map<String, Object> map) {}
	/**
	 *  initAdd 进入新增编辑页之前执行方法,可做页面初始化处理
	 *  
	 *  @param  Map<String, Object> map jsp请求范围响应参数
	 *  
	 *  @return void
	 */
	protected void initAdd(Map<String, Object> map) {}
	/**
	 *  initEdit 进入更新编辑页之前执行方法,可做页面初始化处理
	 *  
	 *  @param  Map<String, Object> map jsp请求范围响应参数
	 *  
	 *  @return void
	 */
	protected void initEdit(Map<String, Object> map) {}
	/**
	 *  beforeSave 保存之前执行方法
	 *  
	 *  用于在保存之前设置一些表单未提交的字段，如实体的创建时间、主键、其它关联实体等
	 *  
	 *  @param  T entity 实体
	 *  
	 *  @return entity
	 */
	protected T beforeSave(T entity,MultipartFile file) throws IOException {
		return entity;
	}
	/**
	 *  afterSave 保存之后执行方法
	 *  
	 *  用于得到保存之后的对象再做进一步处理
	 *  
	 *  @param  T entity 实体
	 *  
	 *  @return void
	 */
	protected void afterSave(T entity) {}
	/**
	 *  beforeUpdate 更新之前执行方法
	 *  
	 *  用于在更新之前设置一些表单未设置的字段，如实体的更新时间、关联关系等
	 *  
	 *  @param  T entity 实体
	 *  
	 *  @return void
	 */
	protected T beforeUpdate(T entity,MultipartFile file) throws IOException {
		return entity;
	}
	/**
	 *  afterUpdate 更新之后执行方法
	 *  
	 *  用于得到更新之后的对象再做进一步处理
	 *  
	 *  @param  T entity 实体
	 *  
	 *  @return void
	 */
	protected void afterUpdate(T entity) {}
	/**
	 *  beforeDeleting 删除之前执行,用于删除之前的逻辑处理
	 *  @param  String id 实体id
	 *  
	 *  @return void
	 */
	protected void beforeDeleting(List<ID> ids) {};
	
	/**
	 * initBinder 初始化数据绑定
	 * 2. 将字段中Date类型转换为String类型
	 */
	@InitBinder
	protected void initBinder(WebDataBinder binder) {
		// Date 类型转换
		binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
			@Override
			public void setAsText(String text) {
				setValue(DateUtils.parseDate(text));
			}
		});
	}
	
	/**
	 * 打开单选对话框
	 * 
	 * @return 返回跳转路径
	 */
	@RequestMapping(value="/singleSelect",method=RequestMethod.GET)
	public String singleSelect(Map<String, Object> map) {		
		initSingleSelect(map);
		return "dialog/" + shortName + "/singleSelect";		
	}
	
	/**
	 * 打开多选对话框
	 * 
	 * @return 返回跳转路径
	 */
	@RequestMapping(value="/multiSelect",method=RequestMethod.GET)
	public String multiSelect(Map<String, Object> map) {		
		initMultiSelect(map);
		return "dialog/" + shortName + "/multiSelect";		
	}
	
	/**
	 *  @ModelAttribute方法 	得到实体数据放入Request
	 *  功能:在更新数据时，若只是提交了实体的部分数据，没有此方法直接更新将丢失表单没提交的数据，
	 *  为了防止未提交数据丢失，提供如下功能：
	 *  1、此方法先从数据库读取一次对象数据放入请求域；
	 *  2、待更新表单提交更新数据时，Srping-MVC会从请求域拿出此对象和表单提交数据合并后以入参方式传给目标方法
	 *  3、目标方法拿到最后合并的对象，调用service的更新方法更新数据库
	 *  4、此方法由Srping-MVC自动调用
	 * 	 
	 *  @param 	String		id		实体id		
	 *  @param  Map<String, Object>	map	Request数据键值对影射
	 *  
	 *  @return void
	 */
	@ModelAttribute
	public void getEntity(@RequestParam(value="id",required=false) 
							ID id,
							Map<String, Object> map){
		if(id != null){//在有id时查数据库 
			T entity = baseService.findOne(id);
			//若实体的关联对象要变更，如有员工实体引用了部门实体，在员工编辑页修改了部门提交；
			//在此要将员工关联部门对象设为null,不为null调用员工更新方法将报错；
			//调用形式：employee.setOrgan(null);
			//此处增加设null关联对象代码
			entity = getEntity(entity);
			//1.map的key在默认情况下，必顺为要存值类名第一个字母小写的名称；
			//2.如这里entity类型是Organ类，其key名必为：organ
			//3.除非在目标方法入参前加有@ModelAttribute修饰，并设置了value值名称，可用其它名
			map.put(shortName, entity);
		}
	}
	
	/**
	 *  listFrame 处理进入列表框架页请求 	 
	 *  
	 *  @param	String	type	类别标识
	 *  @param  Map<String, Object>	map	模型数据返回
	 *  
	 *  @return String	返回页面路径
	 */
	@RequestMapping("listFrame")
	public String listFrame(Map<String, Object> map){
		SecurityUtils.getSubject().checkPermission(shortName + ":query");//检查当前用户是否有查询权限，没有抛出AuthorizationException
		String rootId = baseService.getRootId();
		map.put("rootId", rootId);
		initListFrame(map);
		return getPrefixPath() + "listFrame";
	}
	
	/**
	 *  list 处理多条件查询分页排序请求
	 * 	 
	 *  @param	T		query		查询条件实体封装
	 *  @param  String  expression	查询条件表达式
	 *  @param	String	order		排序字段名
	 *  @param 	String	page		页码
	 *  @param 	String	vQuery		查询表单开闭状态码,0:关闭
	 *  @param  HttpSession session	注入原生API
	 *  @param  Map<String, Object>	map	模型数据返回页面
	 *  
	 *  @return String	返回页面路径
	 * @throws JsonProcessingException 
	 */
	@RequestMapping("list")
	public String list( T query,
						String expression,
			   			@RequestParam(value="order", 
			   				required=false,defaultValue="id desc") 
						String order,
						String page,
						@RequestParam(value="vQuery", 
							required=false,defaultValue="0") 
						String vQuery,
						HttpSession session,  
						Map<String, Object> map) throws JsonProcessingException{
		SecurityUtils.getSubject().checkPermission(shortName + ":query");//检查当前用户是否有查询权限，没有抛出AuthorizationException
		
		String type = (String)session.getAttribute(shortName + "_type");
		if("redirect".equals(type)) {			
			if("String".equals(getQueryMode())) {
				expression = session.getAttribute(shortName + "_query")!=null?session.getAttribute(shortName + "_query").toString():null;
			}
			order = session.getAttribute(shortName + "_order")!=null?session.getAttribute(shortName + "_order").toString():null;
			page = session.getAttribute(shortName + "_page")!=null?session.getAttribute(shortName + "_page").toString():null;
		}
		query = getQuery(query);
		expression = getExpression(expression);
		session.removeAttribute(shortName + "_type");
		session.removeAttribute(shortName + "_query");
		session.removeAttribute(shortName + "_order");
		session.removeAttribute(shortName + "_page");
		int pageNo = 1;
		PageBean<T> pageBean = null;
		try {
			//对 pageNo 的校验
			pageNo = Integer.parseInt(page);
			if(pageNo < 1){
				pageNo = 1;
			}
		} catch (Exception e) {}
		map.put("order", order);
		map.put("page", page);
		map.put("vQuery", vQuery);
		map.put("query", query);
		map.put("expression", expression);
		int pageSize = Config.getPageSize();
		if(session.getAttribute(Constants.PAGESIZE)!=null) {
			pageSize = Integer.parseInt(session.getAttribute(Constants.PAGESIZE).toString());
		}
		if("String".equals(getQueryMode())) {			
			pageBean = baseService.getPageBean(expression,order,pageNo,pageSize);
		}else {
			pageBean = baseService.getPageBean(query,order,pageNo,pageSize);
		}
		map.put("pageBean", pageBean);	
		initList(map);
		return getPrefixPath() + "list";
	}
	
	/**
	 *  view 进入视图页
	 *  REST风格获取URL：view/id，GET请求
	 *  
	 *  需设置返回列表的查询条件，故用POST提交查询条件并设置回列表页
	 *  
	 *  @param	ID pk	主键
	 *  @param	Map<String, Object>	map	页面数据返回
	 *  
	 *  @return	String	返回页面路径
	 */
	@RequestMapping(value="view/{pk}", method=RequestMethod.GET)
	public String view(@PathVariable("pk") ID pk,
									Map<String,Object> map) {
		SecurityUtils.getSubject().checkPermission(shortName + ":view");//检查用户是否有新增权限，没有抛出AuthorizationException		
		map.put("entity", baseService.getOne(pk));
		initView(map);
		return getPrefixPath() + "view";
	}
	
	/**
	 *  input 进入新增编辑页
	 *  REST风格获取URL：entity/，GET请求
	 *  
	 *  @param	Map<String, Object>	map	页面数据返回
	 *  
	 *  @return	String	返回页面路径
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 */
	@RequestMapping(method=RequestMethod.GET)
	public String input(Map<String,Object> map) throws InstantiationException, IllegalAccessException{
		SecurityUtils.getSubject().checkPermission(shortName + ":create");//检查用户是否有新增权限，没有抛出AuthorizationException
		map.put("entity", modelClass.newInstance());
		initAdd(map);
		return getPrefixPath() + "edit";
	}
	
	/**
	 *  input 进入更新编辑页
	 *  REST风格获取URL：entity/{pk}
	 *  
	 *  更新后需返回之前更新记录所在页,故需要传入列表的查询条件、排序字段及页码存入session,以便list方法按条件返回
	 *  
	 *  注：
	 *  1.GET请求改为POST请求，因在进入更新页时要接收查询表单条件实体
	 *  2.不能使用id为请求路径变量名,id和实体主键同名,提交后会同时将query实体的id设上值，故用pk代替
	 *  
	 *  @param	T		query		查询条件实体封装
	 *  @param  String  expression	查询条件表达式
	 *  @param	String	order		排序字段名
	 *  @param 	String	page		页码
	 *  @param  HttpSession session	注入原生API
	 *  @param	Map<String, Object>	map	页面数据返回
	 *  
	 *  @return	String	返回页面路径
	 */
	@RequestMapping(value="/{pk}", method=RequestMethod.POST)
	public String input(@PathVariable("pk") ID pk,//
						T query,
						String expression,
						String order,
						String page,
						HttpSession session,
						Map<String, Object> map){
		SecurityUtils.getSubject().checkPermission(shortName + ":update");//检查当前用户是否有更新权限，没有抛出AuthorizationException
		map.put("entity", baseService.getOne(pk));
		//因更新后需重定向到列表，使用session保存翻页条件在多个请求中共享数据
		session.setAttribute(shortName + "_type", "redirect");
		if("String".equals(getQueryMode())) {
			session.setAttribute(shortName + "_query", expression);
		}else {
			session.setAttribute(shortName + "_query", query);
		}
		session.setAttribute(shortName + "_order", order);
		session.setAttribute(shortName + "_page", page);
		initEdit(map);
		return getPrefixPath() + "edit";
	}
	
	/**
	 *  save	保存实体
	 *  REST风格新增URL：entity/，POST请求
	 *  
	 *  @param	T	entity	接收页面提交数据封装实体
	 *  
	 *  @return	String	返回页面路径
	 * @throws IOException 
	 */
	//@RequiresPermissions(value={"entity:create"});shiro 注解授权方式
	@RequestMapping(method=RequestMethod.POST)
	public String save(T entity,MultipartFile file) throws IOException{
		SecurityUtils.getSubject().checkPermission(shortName + ":create");//检查当前用户是否有新增权限，没有抛出AuthorizationException
		entity = beforeSave(entity,file);
		if(executeSave){
			entity = baseService.save(entity);
			afterSave(entity);
		}
		return "redirect:/" + shortName + "/list";
	}
	
	/**
	 *  update	更新实体
	 *  REST风格更新URL：entity/{id}，PUT请求
	 *  
	 *  @param	T	entity	接收页面提交数据封装实体
	 *  
	 *  @return	String	返回页面路径
	 * @throws IOException 
	 */
	@RequestMapping(value="/{id}",method=RequestMethod.PUT)
	public String update(T entity,MultipartFile file) throws IOException{
		SecurityUtils.getSubject().checkPermission(shortName + ":update");//检查当前用户是否有更新权限，没有抛出AuthorizationException
		entity = beforeUpdate(entity,file);
		if(executeSave) {
			entity = baseService.saveAndFlush(entity);
			afterUpdate(entity);
		}	
		return "redirect:/" + shortName + "/list";
	}
	
	/**
	 *  delete	删除实体
	 *  REST风格删除URL：entity/{id}，DELETE请求
	 *  
	 *  删除后需返回之前删除记录所在页,故需要传入列表的查询条件、排序字段及页码存入session,以便list方法按条件返回
	 *  
	 *  @param	List<ID> ids		删除实体id集合
	 *  @param	T		query		查询条件实体封装
	 *  @param  String  expression	查询条件表达式
	 *  @param	String	order		排序字段名
	 *  @param 	String	page		页码
	 *  @param  HttpSession session	注入原生API
	 *  
	 *  @return	String	返回页面路径
	 */
	@RequestMapping(value="/{ids}",method=RequestMethod.DELETE)
	public String delete(@PathVariable("ids") List<ID> ids,
						T query,
						String expression,
						String order,
						String page,
						HttpSession session){
		SecurityUtils.getSubject().checkPermission(shortName + ":delete");//检查当前用户是否有删除权限，没有抛出AuthorizationException
		beforeDeleting(ids);
		for(ID id:ids) {
			baseService.delete(id);
		}
		//因是重定向到列表，使用session保存翻页条件在多个请求中共享数据
		session.setAttribute(shortName + "_type", "redirect");
		if("String".equals(getQueryMode())) {
			session.setAttribute(shortName + "_query", expression);
		}else {
			session.setAttribute(shortName + "_query", query);
		}
		session.setAttribute(shortName + "_order", order);
		session.setAttribute(shortName + "_page", page);
		return "redirect:/" + shortName + "/list";
	}
	
	public String toJson(Object obj) throws JsonProcessingException {
		ObjectMapper mapper = new ObjectMapper(); 
		return mapper.writeValueAsString(obj);
	}
}
